/**
 * @typedef FormulaNode
 * @type {Object}
 * @property {String} $$key - 树节点标识
 * @property {String} name - 节点名称
 * @property {boolean} success - 解决是否成功
 * @property {String} value - 节点值
 * @property {boolean} isLeaf - 是否为叶子节点
 * @property {String} phase - 解析阶段
 *
 * @typedef NodeResolver
 * @type {Object}
 * @property {String} name - 节点名称
 * @property {String} regex - 正则表达式字符串
 * @property {function} find - 查找方法
 *
 * @typeof TreeMapping
 * @type {Object}
 * @property {number} displayLastIndex - 显示文本的最后索引
 * @property {Object} displayPositionMap - 显示文本的位置映射
 * @property {String} displayName - 显示文本
 * @property {Object} nodeKeyDisplayPositionMap - 树节点与显示文本的映射关系
 */

import { isNil, template, uniqueId } from 'lodash';

import treeReduce from './treeReduce';
import support from './support';
import ERROR_TYPE from './errorType';

const START = '^';
const PHASE_START = 'start';
const PHASE_END = 'end';

const RESOLVER_NAME = {
    BLANK: 'BLANK',
    NUMBER: 'NUMBER',
    OPERATOR: 'OPERATOR',
    CONDITION: 'CONDITION',
    GROUP: 'GROUP',
    ERROR: 'ERROR',
    ILLEGAL: 'ILLEGAL'
};

/**
 * 去除标签内的空格
 * @example replaceBlankBetweenTag('<div id="xxx"> a s </div>') => <div id="xxx">as</div>
 * @param html
 * @returns {*}
 */
const replaceBlankBetweenTag = html =>
    html.trim().replace(/>([^<]+)</gi, (match, group1) => `>${group1.replace(/\s+/g, '')}<`);

const findAtStart = (str, regexStr) => str.match(new RegExp(START + regexStr, 'ig'))?.[0];

/**
 *
 * @param {String} resolverName
 * @param {*} regex
 * @param {*} formulaStr
 * @param {*} param3
 *
 * @returns {FormulaNode}
 */
const resolverFind = (resolverName, regex, formulaStr, { isLeaf = true, phase = null } = {}) => {
    const matchResult = findAtStart(formulaStr, regex);

    return {
        name: resolverName,
        success: matchResult != null,
        value: matchResult,
        isLeaf,
        phase
    };
};

const editableProp = (contentEditable, isBox = false, hasParent = false) => {
    if (support.isIE) {
        if (!hasParent) {
            return contentEditable && !isBox ? '' : 'contenteditable="false" tabinex="-1"';
        }
        return contentEditable && !isBox ? 'contenteditable="true"' : 'contenteditable="false"';
    }

    return !contentEditable && !isBox ? 'contenteditable="false"' : '';
};

/**
 * 空白符解析器
 * @type NodeResolver
 */
const blankResolver = {
    name: RESOLVER_NAME.BLANK,
    regex: '\\s+',
    find(formulaStr) {
        return resolverFind(this.name, this.regex, formulaStr);
    },
    render(blankNode, contentEditable = false, parent = null) {
        const compiledTemplate = template(`
         <span
          class='formula formula-blank'
          data-start='${blankNode.start}'
          data-end='${blankNode.end - 1}'
          ${editableProp(contentEditable, false, !!parent)}
        >
          ${blankNode.value}
        </span>
      `);

        return compiledTemplate({
            blankNode,
            contentEditable
        });
    }
};

/**
 * 数字解析器
 * @type NodeResolver
 */
const numberResolver = {
    name: RESOLVER_NAME.NUMBER,
    regex: '\\d+(?:\\.\\d+)?',
    find(formulaStr) {
        return resolverFind(this.name, this.regex, formulaStr);
    },
    render(numberNode, contentEditable = false, parent = null) {
        if (numberNode.value === 2) {
            console.log('contentEditable:', contentEditable);
            console.log('parent:', parent);
        }
        const compiledTemplate = template(`
         <span
          class='formula formula-number'
          data-start='${numberNode.start}'
          data-end='${numberNode.end - 1}'
          ${editableProp(contentEditable, false, !!parent)}
        >
          ${numberNode.value}
        </span>
      `);

        return compiledTemplate({
            numberNode,
            contentEditable
        });
    }
};

/**
 * 运算符解析器（+-\*\/）
 * @type NodeResolver
 */
const operatorResolver = {
    name: RESOLVER_NAME.OPERATOR,
    regex: '[+\\-\\*/]',
    find(formulaStr) {
        return resolverFind(this.name, this.regex, formulaStr);
    },
    render(operatorNode, contentEditable = false, parent = null) {
        const compiledTemplate = template(`
        <span
          class='formula formula-operator'
          data-start='${operatorNode.start}'
          data-end='${operatorNode.end - 1}'
          ${editableProp(contentEditable, false, !!parent)}
        >
          ${operatorNode.value}
        </span>
      `);

        return compiledTemplate({
            operatorNode,
            contentEditable
        });
    }
};

/**
 * 条件解析器
 * @type NodeResolver
 */
const conditionResolver = {
    name: RESOLVER_NAME.CONDITION,
    regex: '@(?:\\w|[\\u4e00-\\u9fa5]|-)+\\$',
    find(formulaStr) {
        return resolverFind(this.name, this.regex, formulaStr);
    },
    render(conditionNode, contentEditable, parent = null) {
        const self = conditionResolver;

        const compiledTemplate = template(`
        <span
          class='formula formula-condition'
          data-key='${self.getKey(conditionNode)}'
          data-start='${conditionNode.start}'
          data-end='${conditionNode.end - 1}'
          data-name='${self.getDisplayName(conditionNode)}'
          ${editableProp(contentEditable, false, !!parent)}
        >
          ${self.getDisplayName(conditionNode)}
        </span>
      `);

        return compiledTemplate({
            conditionNode,
            contentEditable
        });
    },
    context: null,
    setContext(context) {
        this.context = context;
    },
    getKey(conditionNode) {
        return conditionNode.value.replace('@', '').replace('$', '');
    },
    getDisplayName(conditionNode) {
        return conditionNode.value;
    }
};

const groupResolver = {
    name: RESOLVER_NAME.GROUP,
    regex: {
        start: '\\(',
        end: '\\)'
    },
    find(formulaStr) {
        const startResult = resolverFind(this.name, this.regex.start, formulaStr, {
            isLeaf: false,
            phase: PHASE_START
        });
        const endResult = resolverFind(this.name, this.regex.end, formulaStr, {
            isLeaf: false,
            phase: PHASE_END
        });

        if (startResult.success) {
            return startResult;
        }

        if (endResult.success) {
            return endResult;
        }

        return {
            name: this.name,
            success: false,
            value: null
        };
    },
    render(groupNode, contentEditable, parent, renderChildren) {
        const compiledTemplate = template(`
        <span
          class='formula formula-group'
          data-start='${groupNode.start}'
          data-end='${groupNode.end - 1}'
          ${editableProp(contentEditable, true, !!parent)}
        >
          <span
            class='formula-group-title'
            data-start='${groupNode.start}'
            data-end='${groupNode.start}'
            ${editableProp(contentEditable, false, true)}
          >
            (
          </span>

           <% if(groupNode && groupNode.children && groupNode.children.length > 0) { %>
             <span class='formula-group-children' ${editableProp(false, true, parent)}>
              ${renderChildren(groupNode.children, contentEditable, groupNode, renderChildren).join('')}
            </span>
           <% } %>
          <span
            class='formula-group-title'
            data-start='${groupNode.end - 1}'
            data-end='${groupNode.end - 1}'
            ${editableProp(contentEditable, false, true)}
          >
            )
          </span>
        </span>
      `);

        return compiledTemplate({
            groupNode,
            contentEditable
        });
    }
};

/* 非法字符解析 */
const illegalResolver = {
    name: RESOLVER_NAME.ILLEGAL,
    regex: '[^0123456789.+\\-*/@#]+',
    find(formulaStr) {
        return resolverFind(this.name, this.regex, formulaStr);
    },
    render(illegalNode, contentEditable = false, parent = null) {
        const compiledTemplate = template(`
         <span
          class='formula formula-illegal'
          data-start='${illegalNode.start}'
          data-end='${illegalNode.end - 1}'
          ${editableProp(contentEditable, false, !!parent)}
        >
          ${illegalNode.value}
        </span>
      `);

        return compiledTemplate({
            illegalNode,
            contentEditable
        });
    }
};

const errorResolver = {
    name: RESOLVER_NAME.ERROR,
    create(value) {
        const { name } = this;

        return {
            name,
            value
        };
    },
    find() {
        return {
            name: this.name,
            success: false,
            value: null
        };
    },
    render(errorNode, contentEditable, parent, renderChildren) {
        const compiledTemplate = template(`
        <span
          class='formula formula-error'
          key='${errorNode.$$key}'
          data-start='${errorNode.start}'
          data-end='${errorNode.end - 1}'
          ${editableProp(contentEditable, true, !!parent)}
        >
          <span
            class='formula-group-title'
            data-start='${errorNode.start}'
            data-end='${errorNode.start + errorNode.value.length - 1}'
            ${editableProp(true, false, true)}
          >
            ${errorNode.value}
          </span>
          <% if(errorNode && errorNode.children && errorNode.children.length > 0) { %>
              <span class='formula-group-children' ${editableProp(contentEditable, true, !!parent)}>
                ${renderChildren(errorNode.children, contentEditable, errorNode, renderChildren).join('')}
              </span>
          <% } %>
        </span>
      `);

        return compiledTemplate({
            errorNode,
            contentEditable,
            renderChildren
        });
    }
};

const formulaResolver = {
    resolver: {
        blankResolver,
        numberResolver,
        operatorResolver,
        conditionResolver,
        groupResolver,
        errorResolver,
        illegalResolver
    },
    resolverArray: [
        blankResolver,
        numberResolver,
        operatorResolver,
        conditionResolver,
        groupResolver,
        errorResolver,
        illegalResolver
    ],
    getResolverByNode(node) {
        return this.resolverArray.find(resolver => resolver.name === node.name);
    },

    resolve(formulaStr) {
        if (isNil(formulaStr)) {
            return null;
        }

        const { length } = formulaStr;
        const formulaTree = [];
        const parentNodes = [];
        const getParentNode = () => (parentNodes.length >= 1 ? parentNodes[parentNodes.length - 1] : null);

        const convert2Node = mappingResult => ({
            ...mappingResult,
            $$key: uniqueId(),
            parent: getParentNode()
        });

        const addToTree = node => {
            if (node.parent == null) {
                formulaTree.push(node);
                return;
            }

            if (node.parent.children == null) {
                // eslint-disable-next-line no-param-reassign
                node.parent.children = [];
            }

            node.parent.children.push(node);
        };

        const terminateNode = (node, lastIndex) => {
            // eslint-disable-next-line no-param-reassign
            node.end = lastIndex + 1;
        };

        for (let i = 0; i < length;) {
            const resolvingStr = formulaStr.substr(i);
            let treeNode;

            const mappingResolver = this.resolverArray.find(resolver => {
                const findResult = resolver.find(resolvingStr);
                return findResult?.success;
            });

            let mappingResult = null;
            if (mappingResolver) {
                const findResult = mappingResolver.find(resolvingStr);
                mappingResult = {
                    ...findResult,
                    start: i,
                    end: i + findResult.value.length
                };
            }

            if (
                !mappingResult ||
                (!mappingResult.isLeaf && mappingResult.phase === PHASE_END && parentNodes.length === 0)
            ) {
                const errorResult = {
                    ...errorResolver.create(formulaStr[i]),
                    start: i,
                    end: i + 1,
                    isLeaf: true,
                    errorType: mappingResult ? ERROR_TYPE.NO_BEFORE_MATCH : ERROR_TYPE.NOT_FULL
                };
                addToTree(convert2Node(errorResult));
                i = errorResult.end;
                // eslint-disable-next-line no-continue
                continue;
            }

            if (mappingResult.isLeaf) {
                addToTree(convert2Node(mappingResult));
            } else if (!mappingResult.isLeaf && mappingResult.phase === PHASE_START) {
                treeNode = convert2Node(mappingResult);
                parentNodes.push(treeNode);
                addToTree(treeNode);
            } else if (!mappingResult.isLeaf && mappingResult.phase === PHASE_END) {
                treeNode = terminateNode(parentNodes.pop(), i);
            }
            i = mappingResult.end;
        }

        const iteratorTree = (tree, func, contentEditable) => {
            tree.forEach(treeNode => {
                func(treeNode);
                if (treeNode.children) {
                    iteratorTree(treeNode.children, func, contentEditable);
                }
            });
        };
        // 如果解析数组中还有值，说明缺少结束符，公式错误，
        if (parentNodes?.length > 0) {
            iteratorTree(formulaTree, treeNode => {
                parentNodes.forEach(parentNode => {
                    if (treeNode.$$key === parentNode.$$key) {
                        // eslint-disable-next-line no-param-reassign
                        treeNode.name = RESOLVER_NAME.ERROR;
                        // eslint-disable-next-line no-param-reassign
                        treeNode.end = length;
                        // eslint-disable-next-line no-param-reassign
                        treeNode.errorType = ERROR_TYPE.NO_AFTER_MATCH;
                        console.log('tree node:', treeNode);
                    }
                });
            });
        }

        return formulaTree;
    },

    /**
     * 将公式的语法树渲染
     * @param {*} formulaTree 公式树
     * @param contentEditable 是否可编辑
     */
    render(formulaTree, contentEditable = false) {
        if (isNil(formulaTree)) {
            return null;
        }

        const r = (tree, editable, parent) => {
            if (!tree || tree.length === 0) {
                return [];
            }
            return tree.map(node => {
                const mappedResolver = this.getResolverByNode(node);

                if (mappedResolver) {
                    return mappedResolver.render(node, editable, parent, r);
                }

                return node.value;
            });
        };

        return replaceBlankBetweenTag(r(formulaTree, contentEditable, null).join(''));
    }
};

/* 渲染中文本与节点的对应关系 */
const treePositionCallback = (accumulator, node, afterTrigger) => {
    const rel = accumulator;

    const {
        displayLastIndex,
        displayPositionMap,
        displayName: globalDisplayName,
        nodeKeyDisplayPositionMap,
        keyNodeMap
    } = rel;

    let displayName;
    if (node.name === RESOLVER_NAME.GROUP) {
        displayName = !afterTrigger ? '(' : ')';
    } else if (node.name === RESOLVER_NAME.ERROR) {
        displayName = !afterTrigger ? node.value : '';
    } else if (node.name === RESOLVER_NAME.CONDITION) {
        const resolver = formulaResolver.getResolverByNode(node);
        displayName = resolver.getDisplayName ? resolver.getDisplayName(node) : node.value;
    } else {
        displayName = node.value;
    }

    const { length } = displayName;
    let newDisplayIndex = displayLastIndex;
    for (let i = 1; i <= length; i += 1) {
        newDisplayIndex += 1;
        const char = displayName[i - 1];
        displayPositionMap[newDisplayIndex] = {
            node,
            offset: i,
            char
        };
        Object.assign(rel, {
            displayLastIndex: newDisplayIndex,
            displayPositionMap
        });
    }

    const newNodeKeyDisplayPositionMap = { ...nodeKeyDisplayPositionMap };
    // 处理nodeKeyDisplayPositionMap
    if (node.name === RESOLVER_NAME.GROUP && !afterTrigger) {
        newNodeKeyDisplayPositionMap[node.$$key] = { displayStart: displayLastIndex + 1 };
    } else if (node.name === RESOLVER_NAME.GROUP && afterTrigger) {
        newNodeKeyDisplayPositionMap[node.$$key] = {
            ...newNodeKeyDisplayPositionMap[node.$$key],
            displayEnd: displayLastIndex + 1 + length
        };
    } else if (node.name === RESOLVER_NAME.ERROR && !afterTrigger) {
        newNodeKeyDisplayPositionMap[node.$$key] = {
            displayStart: displayLastIndex + 1,
            displayEnd: node.isLeaf ? displayLastIndex + 1 + length : null
        };
    } else if (node.name === RESOLVER_NAME.ERROR && afterTrigger) {
        newNodeKeyDisplayPositionMap[node.$$key] = {
            ...newNodeKeyDisplayPositionMap[node.$$key],
            displayEnd: displayLastIndex + 1 + length
        };
    } else {
        newNodeKeyDisplayPositionMap[node.$$key] = {
            displayStart: displayLastIndex + 1,
            displayEnd: displayLastIndex + 1 + length
        };
    }

    Object.assign(rel, {
        nodeKeyDisplayPositionMap: newNodeKeyDisplayPositionMap,
        displayName: globalDisplayName + displayName
    });

    if (!afterTrigger) {
        keyNodeMap[node.$$key] = node;
        Object.assign(rel, {
            keyNodeMap
        });
    }

    return rel;
};

Object.assign(formulaResolver, {
    /**
     * @param  { FormulaNode } formulaTree
     * @returns { TreeMapping }
     */
    mapping(formulaTree) {
        return treeReduce(formulaTree, treePositionCallback, {
            // 显示文本的最后索引
            displayLastIndex: -1,
            // 显示文本的位置映射
            displayPositionMap: {},
            // 显示文本
            displayName: '',
            // 树节点与显示文本的映射关系
            nodeKeyDisplayPositionMap: {},
            keyNodeMap: {}
        });
    },

    /**
     * 获取显示文本名称
     * @param {TreeMapping} treeMapping
     * @param conditionNode
     * @returns {undefined}
     */
    getDisplayName(treeMapping, conditionNode) {
        if (!conditionNode) {
            return treeMapping.displayName;
        }
        const { start, end } = treeMapping.nodeKeyDisplayPositionMap[conditionNode.$$key];
        return treeMapping.displayName.substring(start, end);
    },

    /**
     * 将渲染的html转成公式
     * @param renderHTML
     * @returns {*}
     */
    convertToFormula(renderHTML) {
        const conditionHtmlReg = /<[^>]+formula-condition([^>]+)>([^><]+)<\/[^>]+>/gi;

        let rel = renderHTML;

        // 将形如
        // <span class="formula formula-condition" data-key="c3" data-start="2" data-end="4" data-name="@基本月薪">@基本月薪+123</span>
        // =>
        // <span>@c3+123</span>
        // 的字符串
        rel = rel.replace(conditionHtmlReg, (match, group1, group2) => {
            const key = /data-key="([^"]+)"/gi.exec(group1)[1];
            const displayName = /data-name="([^"]+)"/gi.exec(group1)[1];

            return `<span>${group2.replace(displayName, `@${key}$`)}</span>`;
        });

        // 去除标签, 例<span>@c3</span> => @c3
        rel = rel.replace(/<[^>]+>/g, '');
        // 将中文左括号改成英文左括号
        rel = rel.replace('（', '(');
        // 将中文右括号改成英文右括号
        rel = rel.replace('）', ')');

        return rel;
    }
});

export { formulaResolver as default, RESOLVER_NAME };
